﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PostSharp.CodeWeaver;
using PostSharp.CodeModel;
using PostSharp.Collections;
using PostSharp.CodeModel.Helpers;
using System.Reflection;

namespace PostSharp4ViewState.Weaver
{
   sealed class LoadAdvice : PersistAdvice
   {
      public LoadAdvice(PersistTask parent, PersistMode mode, PerTypeData data)
         : base(parent, mode, mode == PersistMode.ViewState ? "LoadViewState" : "LoadControlState", data)
      {
      }

      public override JoinPointKinds JointPointKind
      {
         get { return JoinPointKinds.BeforeMethodBody; }
      }
      protected override void DoCreateMethod(MethodDefDeclaration methodDef)
      {
         methodDef.Name = _methodName;
         methodDef.Attributes = MethodAttributes.Virtual | MethodAttributes.Family | MethodAttributes.HideBySig;
         methodDef.CallingConvention = CallingConvention.HasThis;

         methodDef.ReturnParameter = new ParameterDeclaration();
         methodDef.ReturnParameter.Attributes = ParameterAttributes.Retval;
         methodDef.ReturnParameter.ParameterType = _parent.VoidType;

         ParameterDeclaration param = new ParameterDeclaration();         
         param.ParameterType = _parent.ObjectType;
         methodDef.Parameters.Add(param);
         MethodBodyDeclaration methodBody = new MethodBodyDeclaration();
         methodDef.MethodBody = methodBody;
         methodBody.RootInstructionBlock = methodBody.CreateInstructionBlock();
      }

      private void WeavePrivate(MethodDefDeclaration wovenMethod, InstructionWriter writer, InstructionBlock block, bool exists)
      {
         InstructionSequence seq = wovenMethod.MethodBody.CreateInstructionSequence();
         block.AddInstructionSequence(seq, NodePosition.Before, null);
         writer.AttachInstructionSequence(seq);
         writer.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);

         LocalVariableSymbol tmpVar = block.DefineLocalVariable(_parent.ObjectArrayType, "~tmp~{0}");
         LocalVariableSymbol tmpVar2 = block.DefineLocalVariable(_parent.ObjectArrayType, "~tmp2~{0}");

         writer.EmitInstruction(OpCodeNumber.Ldarg_1);
         writer.EmitInstructionType(OpCodeNumber.Castclass, _parent.ObjectArrayType);
         writer.EmitInstructionLocalVariable(OpCodeNumber.Stloc, tmpVar);
         writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpVar);
         writer.EmitInstruction(OpCodeNumber.Ldc_I4_1);
         writer.EmitInstruction(OpCodeNumber.Ldelem_Ref);
         writer.EmitInstructionType(OpCodeNumber.Castclass, _parent.ObjectArrayType);
         writer.EmitInstructionLocalVariable(OpCodeNumber.Stloc, tmpVar2);

         writer.DetachInstructionSequence();

         InstructionSequence seqPrev = wovenMethod.MethodBody.CreateInstructionSequence();
         block.AddInstructionSequence(seqPrev, NodePosition.After, seq);

         IEnumerable<FieldDefDeclaration> fields = _data.GetMembers<FieldDefDeclaration>(_mode);
         IEnumerable<PropertyDeclaration> properties = _data.GetMembers<PropertyDeclaration>(_mode);

         int i = 0;
         foreach (FieldDefDeclaration fieldDef in fields)
         {            
            LocalVariableSymbol tmpBool = block.DefineLocalVariable(_parent.BoolType, "~tmpBool~{0}");

            InstructionSequence seqNext = wovenMethod.MethodBody.CreateInstructionSequence();
            block.AddInstructionSequence(seqNext, NodePosition.After, seqPrev);

            writer.AttachInstructionSequence(seqPrev);
            writer.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
            writer.EmitInstruction(OpCodeNumber.Nop);

            writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpVar2);
            writer.EmitInstructionInt32(OpCodeNumber.Ldc_I4, i);
            writer.EmitInstruction(OpCodeNumber.Ldelem_Ref);
            writer.EmitInstruction(OpCodeNumber.Ldnull);
            writer.EmitInstruction(OpCodeNumber.Ceq);
            writer.EmitInstructionLocalVariable(OpCodeNumber.Stloc, tmpBool);
            writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpBool);
            writer.EmitBranchingInstruction(OpCodeNumber.Brtrue_S, seqNext);
            writer.EmitInstruction(OpCodeNumber.Ldarg_0);
            writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpVar2);
            writer.EmitInstructionInt32(OpCodeNumber.Ldc_I4, i);
            writer.EmitInstruction(OpCodeNumber.Ldelem_Ref);
            if (fieldDef.FieldType.GetSystemType(null, null).IsValueType)
            {
               writer.EmitInstructionType(OpCodeNumber.Unbox_Any, fieldDef.FieldType);
            }
            else
            {
               writer.EmitInstructionType(OpCodeNumber.Castclass, fieldDef.FieldType);
            }
            writer.EmitInstructionField(OpCodeNumber.Stfld, GenericHelper.GetFieldCanonicalGenericInstance(fieldDef));
            writer.DetachInstructionSequence();
            seqPrev = seqNext;
            i++;
         }
         foreach (PropertyDeclaration propDef in properties)
         {
            LocalVariableSymbol tmpBool = block.DefineLocalVariable(_parent.BoolType, "~tmpBool~{0}");

            InstructionSequence seqNext = wovenMethod.MethodBody.CreateInstructionSequence();
            block.AddInstructionSequence(seqNext, NodePosition.After, seqPrev);

            writer.AttachInstructionSequence(seqPrev);
            writer.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
            writer.EmitInstruction(OpCodeNumber.Nop);

            writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpVar2);
            writer.EmitInstructionInt32(OpCodeNumber.Ldc_I4, i);
            writer.EmitInstruction(OpCodeNumber.Ldelem_Ref);
            writer.EmitInstruction(OpCodeNumber.Ldnull);
            writer.EmitInstruction(OpCodeNumber.Ceq);
            writer.EmitInstructionLocalVariable(OpCodeNumber.Stloc, tmpBool);
            writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpBool);
            writer.EmitBranchingInstruction(OpCodeNumber.Brtrue_S, seqNext);
            writer.EmitInstruction(OpCodeNumber.Ldarg_0);
            writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpVar2);
            writer.EmitInstructionInt32(OpCodeNumber.Ldc_I4, i);
            writer.EmitInstruction(OpCodeNumber.Ldelem_Ref);
            if (propDef.PropertyType.GetSystemType(null, null).IsValueType)
            {
               writer.EmitInstructionType(OpCodeNumber.Unbox_Any, propDef.PropertyType);
            }
            else
            {
               writer.EmitInstructionType(OpCodeNumber.Castclass, propDef.PropertyType);
            }
            writer.EmitInstructionMethod(OpCodeNumber.Call, GenericHelper.GetMethodCanonicalGenericInstance(propDef.Members.GetBySemantic(MethodSemantics.Setter).Method));
            writer.DetachInstructionSequence();
            seqPrev = seqNext;
            i++;
         }

         writer.AttachInstructionSequence(seqPrev);
         writer.EmitSymbolSequencePoint(SymbolSequencePoint.Hidden);
         writer.EmitInstruction(OpCodeNumber.Nop);
         writer.EmitInstructionLocalVariable(OpCodeNumber.Ldloc, tmpVar);
         writer.EmitInstruction(OpCodeNumber.Ldc_I4_0);
         writer.EmitInstruction(OpCodeNumber.Ldelem_Ref);
         writer.EmitInstructionByte(OpCodeNumber.Starg_S, 1);
         if (!exists)
         {
            writer.EmitInstruction(OpCodeNumber.Ldarg_0);
            writer.EmitInstruction(OpCodeNumber.Ldarg_1);

            IMethod baseMethod = GetInheritedMethod(wovenMethod.DeclaringType.BaseType.GetTypeDefinition(), _methodName, writer.MethodBody.Module);
            writer.EmitInstructionMethod(OpCodeNumber.Call, baseMethod);            
            writer.EmitInstruction(OpCodeNumber.Ret);
         }
         writer.DetachInstructionSequence();
      }

      public override void Weave(WeavingContext context, InstructionBlock block)
      {
         if (context.Method.Name == _methodName)
         {
            MethodDefDeclaration wovenMethod = context.Method;
            WeavePrivate(wovenMethod, context.InstructionWriter, block, true);
         }
         else
         {
            MethodDefDeclaration wovenMethod = context.Method.DeclaringType.Methods.GetByName(_methodName).FirstOrDefault();
            WeavePrivate(wovenMethod, context.InstructionWriter, wovenMethod.MethodBody.RootInstructionBlock, false);
         }
      }

   }
}
